iT邦幫忙

0

VScode 開發應用系統專案(10) - Spring boot MVC 應用系統 - 啟動(1)

  • 分享至 

  • xImage
  •  

專案開發時,Spring Boot 啟動與套用Thymeleaf layout版面的選單登入

概述

先前Spring boot MVC 應用系統設計前準備 https://ithelp.ithome.com.tw/articles/10398853 ,有提及開發前後端系統程式專案結構,這裡先依Spring boot 啟動過程中,加入一些系統啟動時準備資料(例如menu資料),
完成登入並Landing顯示功能選項的功能開發,因為不擅長前端版面開發,所以藉由經Bootstrap以及Jquery包裝設計好的Free Bootstrap Admin Template - AdminLTE 來學習與開發前端Hhml功能,前端採用Thymeleaf的原因,是因為其語法幾乎與 Html語法相同,所有Hhtml都很容易轉換為Thymeleaf的樣板。很容易轉換為Thymeleaf的樣板。

**AdminLTE 參考 URL: https://adminlte.io/ **

**Thymeleaf 參考 URL: https://www.thymeleaf.org/documentation.html **

系統啟動與處理登入選單相關處理

1 Spring Boot 啟動過程中,執行的程序。

位置 package tw.lewishome.webapp;

  1. SystemServletContextListener.java (new 系統啟動相關程序)
  • 這是 Java Servlet 規範的一部分,用來 Web 應用(ServletContext)的啟起執行的程序在,初始化Web Application 的任何Flitter或Servlet之前,由系統會執行ServletContextListener初始化。
  • 不一定是 Spring Boot Bean,所以無法直接 @Autowired,
  • Spring boot 直接執行時,先啟動這個SystemServletContextListener 再啟動 SystemApplicationListener。
  • 使用 Spring Boot 架構,另外有SystemApplicationListener 可以代替這個 Listener,並且Junit 測試啟動時 @SpringBootTest 不會執行此 Listener,所以保留暫不使用。
package tw.lewishome.webapp;

import org.springframework.stereotype.Component;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import lombok.extern.slf4j.Slf4j;

/**
 * 系統 ServletContextListener,於 Web 應用程式啟動時執行初始化邏輯。
 *
 * 主要功能:
 * 設定系統執行環境(Profile),並初始化系統參數。</li>
 *
 * 注意事項:
 * 在初始化任何 Filter 或 Servlet 之前執行。</li>
 *
 * @author Lewis
 */
@Component
@Slf4j
public class SystemServletContextListener implements ServletContextListener {
    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * Constructs a new SystemServletContextListener instance.
     * This is the default constructor, implicitly provided by the compiler
     * and can be used to create a new instance of the class.
     */
    public SystemServletContextListener() {
        // Constructor body (can be empty)
    }

    /**
     * <pre>
     * 在初始化Web Application 的任何Flitter或Servlet之前,將通知ServletContextListener初始化。
     * Jboss 先啟動 SystemApplicationListener 再啟動 SystemServletContextListener
     * spring boot 先啟動 SystemServletContextListener 再啟動 SystemApplicationListener
     *
     * Junit Test 啟動 @SpringBootTest 時不會啟動此程序
     * </pre>
     */
    @Override
    public void contextInitialized(ServletContextEvent sce) {

        // 配合Spring AutoConfiguration, 若有加 @Autowired JPA repository,則DataBase會先啟動
        // 否則Database 沒有啟動。(於 JNDI已經有連線時,沒有影響)
        log.info("啟動時自動執行一次 ServletContextListener 的 contextInitialized 方法");

        
    }

}

  1. SystemApplicationListener.java (new 系統啟動程序)
  • 這是 Spring 提供的事件系統啟動機制,可以使用 @Autowired 等 Spring Bean,適合做 初始化資料、啟動後檢查、啟動任務(排程、快取、MQ)
  • 佈署到Jboss/Tomcat等Container Server執行時,會先啟動 SystemApplicationListener 再啟動 SystemServletContextListener
  • 所以使用 Spring Boot 架構,需要系統啟動時的準備與設定資料的程序,以下是依系統設計架構啟動時初始化資料:
    • 取得系統所有的 URL Endpoint並設定給 GlobalConstants.LIST_SYSTEM_ENDPOINT變數。
    • 取得系統所有環境變數並設定給GlobalConstants.ENV_VAR變數 (Key值一律轉大寫)
    • 顯示系統排程設定
    • 清除所有系統快取 caffeineCache
    • 只有主伺服器才清除 Redis 快取
    • 初始化系統選單項目
package tw.lewishome.webapp;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.base.cache.caffeine.CaffeineCacheUtils;
import tw.lewishome.webapp.base.cache.redis.RedisCacheUtils;
import tw.lewishome.webapp.base.schedule.ScheduleTaskService;
import tw.lewishome.webapp.base.utility.common.CommUtils;
import tw.lewishome.webapp.base.utility.common.SystemEnvReader;
import tw.lewishome.webapp.page.PageConstants;
import tw.lewishome.webapp.page.base.service.PageMenuItemService;

/**
 * 在 Web 應用程式初始化完成時執行的監聽器。
 *
 * Jboss/Tomcat 會先啟動 SystemApplicationListener,再啟動 SystemServletContextListener;
 * Spring Boot 則是先啟動 SystemServletContextListener,再啟動 SystemApplicationListener。
 * <br>
 * 特別注意:Junit 測試啟動 @SpringBootTest 時,會執行此 SystemApplicationListener。
 *
 * <ul>
 * <li>收集所有系統 Endpoint 並存入常數。</li>
 * <li>初始化系統選單項目。</li>
 *
 * </ul>
 * 
 * @author lewis
 */
@Component
@Slf4j
public class SystemApplicationListener implements ApplicationListener<ContextRefreshedEvent> {

    /**
     * 
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * 
     * Constructs a new SystemApplicationListener instance.
     * This is the default constructor, implicitly provided by the compiler
     */
    public SystemApplicationListener() {
        // Constructor body (can be empty)
    }

    @Autowired
    ScheduleTaskService scheduleTaskService; 

    @Autowired
    SystemEnvReader systemEnvReader;

    @Autowired
    PageMenuItemService pageMenuItemService;

    @Autowired
    CaffeineCacheUtils caffeineCacheUtils;

    @Autowired
    RedisCacheUtils redisCacheUtils;

    /**
     * 
     *
     * 啟動時自動執行一次 SystemApplicationListener 的 onApplicationEvent 方法。
     *
     * 主要功能:
     * <ul>
     * <li>收集所有 Endpoint 並存入 BaseSystemConstants.ListSysEndPoint。</li>
     * <li>初始化 BasePageConstants.pageMenuItems。</li>
     * <li>檢查並產生全域 RSA 金鑰。</li>
     * </ul>
     */
    @Override
    public void onApplicationEvent(@NonNull ContextRefreshedEvent event) {

        // 配合Spring AutoConfiguration, 若有加 @Autowired JPA repository,則DataBase會先啟動
        // 否則Database 沒有啟動。(於 Jboss執行時,JNDI已經有連線,沒有影響)
        log.info("啟動時自動執行一次 SystemApplicationListener 的 onApplicationEvent 方法");

        // 取得系統所有的 URL Endpoint
        List<String> listEndpoint = new ArrayList<>();
        ApplicationContext applicationContext = event.getApplicationContext();
        RequestMappingHandlerMapping requestMappingHandlerMapping = applicationContext
                .getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);

        Map<RequestMappingInfo, HandlerMethod> map = requestMappingHandlerMapping
                .getHandlerMethods();
        log.info( "======= System Endpoint List ========== " );   
        map.forEach((key, value) -> {

            log.info("Key:{} =  Value: {}", key, value);
            String keyString = key.toString();
            String oneListEndPoint = keyString.substring(keyString.indexOf('[') + 1, keyString.indexOf(']'));
            List<String> splitOneListEndPoint = CommUtils.splitDelimiter(oneListEndPoint, "||");
            for (int i = 0; i < splitOneListEndPoint.size(); i++) {
                if (Boolean.FALSE.equals(listEndpoint.contains(splitOneListEndPoint.get(i)))) {
                    listEndpoint.add(splitOneListEndPoint.get(i));
                }
            }
        });
        log.info( "======= System Endpoint List end ========== " );   

        // 將 系統所有的 ENDPOINT 加入系統常變數 listSysEndPoint
        String contextPath = event.getApplicationContext().getApplicationName();       
        listEndpoint.forEach(x -> {
            GlobalConstants.LIST_SYSTEM_ENDPOINT.add(contextPath + x);
        });
        
        // 設定環境變數於 GlobalConstants.ENV_VAR// 
        log.info("設定環境變數於 GlobalConstants.ENV_VAR ");
        GlobalConstants.ENV_VAR = systemEnvReader.getAllEnvironmentVariables();
        log.info( "======= System Environment Value List ========== " );   
        GlobalConstants.ENV_VAR.forEach((key, value) -> {
            log.info("Key:{}  =  Value: {}", key, value);
        });
        log.info( "======= System Environment Value List ========== " );   

        // 顯示系統排程設定
        scheduleTaskService.listScheduledTaskJobs();

        // 清除所有系統快取 caffeineCache
        caffeineCacheUtils.evictAllCaches();
        log.info("清除 所有系統快取完成");

		//只有主伺服器才清除 Redis 快取
        if (scheduleTaskService.isScheduleMainServer(GlobalConstants.HOST_SERVER_NAME)) {
           redisCacheUtils.evictAllCaches();
           log.info("清除 Redis 所有系統快取完成");
        }
        
        // 初始化系統選單項目
        PageConstants.pageMenuItems = pageMenuItemService.getPageMenuItems();
        log.info("初始化系統選單項目完成");
    }
}

3.SystemApplicationStartingEvent.java (new 系統啟動程序)

  • 當 Spring Boot 應用程式完全啟動並且所有元件都已初始化時被,
  • 這裡已經是準備好了所有功能,可以通知系統已經Ready的訊息(如跨系統通知或允許Ready Function回覆)
  • 保留暫不使用。
package tw.lewishome.webapp;

import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import lombok.extern.slf4j.Slf4j;

/**
 * 應用程式啟動事件監聽器。
 * <p>
 * 此類別用於監聽 Spring Boot 應用程式的啟動事件,
 * 並在應用程式完全啟動後執行相應的初始化邏輯。
 * </p>
 *
 * @author Lewis
 * @since 1.0
 */
@Component
@Slf4j
public class ApplicationStartingEvent {

    /**
     * 應用程式就緒事件的回調方法。
     * 當 Spring Boot 應用程式完全啟動並且所有元件都已初始化時被調用。
     * 此方法可用於執行應用程式啟動後的初始化作業。
     *
     * @see org.springframework.boot.context.event.ApplicationReadyEvent
     */
    @EventListener(ApplicationReadyEvent.class)
    public void onApplicationReady() {
        log.info("Application started successfully!");
    }
}

2 Spring Boot 系統功能選單資料庫Table物件 。

位置 tw.lewishome.webapp.database.primary.entity;

  1. SysMenuDataEntity.java (new 系統選單Menu Entity Table)
  • 系統選單物件,以 menuParentUuid = "-1" 為跟目錄,依據此還原與處理系統選單階層。
package tw.lewishome.webapp.database.primary.entity;

import java.io.Serializable;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.UUID;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;

import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.EmbeddedId;
import jakarta.persistence.Entity;
import jakarta.persistence.Index;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import tw.lewishome.webapp.database.audit.EntityAudit;

//https://layui.dev/docs/2/base.html
//https://www.ombudsman.go.id/assets/js/plugins/treetable/

/**
 * SysMenuData Table Entity
 *
 * @author lewis
 * @version $Id: $Id
 */
@Entity
// 資料庫的 Table 名稱
// @Table(name = "sysmenudata", indexes = {
// @Index(name = "idx_parentUuid", columnList = "menuParentUuid,menuSeq"),
// @Index(name = "idx_lastname_firstname", columnList = "lastName, firstName")})
@Table(name = "sysmenudata", indexes = @Index(name = "idx_parentUuid", columnList = "menuParentUuid,menuSeq"))

@Data
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
public class SysMenuDataEntity extends EntityAudit<String> {

    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * Constructs a new SysMenuDataEntity instance.
     * This is the default constructor, implicitly provided by the compiler
     * if no other constructors are defined.
     */
    public SysMenuDataEntity() {
        // Constructor body (can be empty)
    }
    private static final long serialVersionUID = 1L;
    /** Primary Key */
    @EmbeddedId
    public DataKey dataKey;

    /** Menu Seq */
    @Column(name = "menuSeq")
    public int menuSeq;

    /** isMenu flag */
    @Column(name = "isMenu")
    public Boolean isMenu;

    /** Menu item description */
    @Column(name = "menuDesc", length = 64)
    public String menuDesc;

    /** Menu Function URL (for Do Menu) */
    @Column(name = "menuUrl", length = 256)
    public String menuUrl;

    /** Menu Function (BasePageConstants.ListAvailPageFunc) */
    @Column(name = "menuFunc", length = 32)
    public String menuFunc;

    /** 存取權限 */
    @Column(name = "menuRoles", length = 512)
    public String menuRoles;

    /** 上一層節點Uuid */
    @Column(name = "menuParentUuid", length = 1024)
    public String menuParentUuid;

    /**
     * Entity Key
     *
     */
    @Embeddable
    @Data
    public static class DataKey implements Serializable {

    /**
	 * Fix for javadoc warning : 
	 * use of default constructor, which does not provide a comment
	 * Constructs a new DataKey instance.
	 * This is the default constructor, implicitly provided by the compiler
	 * if no other constructors are defined.
	 */
	public DataKey() {
		// Constructor body (can be empty)
	}

        private static final long serialVersionUID = 1L;
        /** UUID */
        @Column(name = "uuid", length = 64)
        public String uuid = UUID.randomUUID().toString();
    }

     /**
     * MyBatis TypeHandler for DataKey
     */
    /**
     * MyBatis BaseTypeHandler} for converting between the application's
     * DataKey object and its JDBC representation.
     *
     */
    public static class DataKeyHandler extends BaseTypeHandler<DataKey> {

        /**
         * Fix for javadoc warning :
         * use of default constructor, which does not provide a comment
         * Constructs a new AsyncServiceWorkerSample instance.
         * This is the default constructor, implicitly provided by the compiler
         * if no other constructors are defined.
         */
        public DataKeyHandler() {
            // Constructor body (can be empty)
        }

        @Override
        public void setNonNullParameter(PreparedStatement ps, int i, DataKey parameter, JdbcType jdbcType)
                throws SQLException {
            try {
                ps.setString(1, parameter.getUuid());
            } catch (Exception e) {
                e.printStackTrace();
            }

        }

        @Override
        public DataKey getNullableResult(ResultSet rs, String columnName) throws SQLException {
            DataKey dataKey = new DataKey();
            if (rs.wasNull() == false) {
                dataKey.setUuid(rs.getString("uuid"));
            }
            return dataKey;
        }

        @Override
        public DataKey getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
            DataKey dataKey = new DataKey();
            if (rs.wasNull() == false) {
                dataKey.setUuid(rs.getString(1));
            }
            return dataKey;
        }

        @Override
        public DataKey getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
            DataKey dataKey = new DataKey();
            if (cs.wasNull() == false) {
                dataKey.setUuid(cs.getString(1));
            }
            return dataKey;
        }
    }    
}

3 Spring Boot 系統功能選單資料庫Table存取物件 。

位置 tw.lewishome.webapp.database.primary.repository;

  1. SysMenuDataRepository.java (new 系統選單Menu repository存取)
  • 系統選單物件,Spring JPA repository存取資料
package tw.lewishome.webapp.database.primary.repository;

import java.util.List;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

import tw.lewishome.webapp.database.primary.entity.SysMenuDataEntity;

/**
 * SysMenuData JPA Repository
 *
 * @author lewis
 * @version $Id: $Id
 */
@Transactional
@Repository
public interface SysMenuDataRepository extends JpaRepository<SysMenuDataEntity, SysMenuDataEntity.DataKey>,
                JpaSpecificationExecutor<SysMenuDataEntity> {

        /**
         *
         * findByMenuParentUuid.
         *
         *
         * @param menuParentUuid a  String  object
         * @return a List object
         */
        public List<SysMenuDataEntity> findByMenuParentUuid(String menuParentUuid);

}

4 Spring Boot 系統功能 Page 選單物件 。

位置 package tw.lewishome.webapp.page.base.model

  1. PageMenuItemModel.java (new 系統選單Menu共用物件)
  • 系統選單物件,MenuItems物件包含本身MenuItems,需要使用遞廻方式存取資料。

package tw.lewishome.webapp.page.base.model;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import lombok.Data;

/**
 * <pre>
 * 系統選單(Menu Model),使用遞廻方式。
 * </pre>
 *
 * @author lewis
 * @version $Id: $Id
 */
@Data
public class PageMenuItemModel implements Serializable {
    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * Constructs a new PageMenuItemModel instance.
     * This is the default constructor, implicitly provided by the compiler
     * if no other constructors are defined.
     */
    public PageMenuItemModel() {
        // Constructor body (can be empty)
    }
    /** serialVersionUID */
    private static final long serialVersionUID = 1L;
    /** 選單抬頭 文字 */
    private String menuBarText = "";
    /** 選單 Model */
    private List<MenuItems> naviItems = new ArrayList<MenuItems>();

    /**
     * 系統選單資料
     **/
    @Data
    public static class MenuItems implements Serializable {
        /**
         * Fix for javadoc warning :
         * use of default constructor, which does not provide a comment
         * Constructs a new MenuItems instance.
         * This is the default constructor, implicitly provided by the compiler
         * if no other constructors are defined.
         * 
         */
        public MenuItems() {
            // Constructor body (can be empty)
        }

        /** serialVersionUID */
        private static final long serialVersionUID = 1L;
        /** 選單順序 */
        public int menuSeq = 0;
        /** 選單順序 */
        public Boolean isMenu = true;
        /** 是否為選單 ( false: 是程式) */
        public String menuDesc = "";
        /** 程式 URL (Controllr 處理程序 isMenu=false時必須指定) */
        public String menuUrl = "";
        /** 選單功能 "Add" "Edit" "Confirm" "Inquiry" ("" = 不區分功能, 覆核功能需要有"Confirm" ) */
        public String menuFunc = "";
        /** 授權使用角色 (All /IT_Admin/Sys_Admin/User_Admin/User_Id) */
        public String menuRoles = "";
        /** 子選單或程式 (摺疊) */
        public List<MenuItems> collapseItems = new ArrayList<MenuItems>();
    }
}

5 Spring Boot 應用系統功能選單存取服務程式 。

位置 package tw.lewishome.webapp.page.base.service;

  1. PageMenuService.java (new 取得前端系統選單Menu資料)
  • 負責處理系統選單(Menu)相關的服務邏輯
    • 根據使用者權限取得對應的選單項目,並支援兩層快取以提升效能。
    • 支援從外部檔案(menuItemsData.json)或專案資源載入選單結構,方便選單維護與部署。
    • 提供選單資料與資料庫(SysMenuDataEntity)之間的轉換功能,支援選單資料的持久化與還原
    • 根據使用者名稱與權限,動態產生符合權限的選單結構。
    • 支援特殊權限角色(如 IT_ADMIN、SYS_ADMIN 等)的判斷與加入。
package tw.lewishome.webapp.page.base.service;

import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.GlobalConstants;
import tw.lewishome.webapp.database.primary.entity.SysMenuDataEntity;
import tw.lewishome.webapp.database.primary.repository.SysMenuDataRepository;
import tw.lewishome.webapp.database.primary.service.GetSysParmValueService;
import tw.lewishome.webapp.base.utility.common.CommUtils;
import tw.lewishome.webapp.page.base.model.PageMenuItemModel;
import tw.lewishome.webapp.page.PageConstants;


/**
 * <pre>
 * SystemGetMenuItemService 負責處理系統選單(Menu)相關的服務邏輯。
 *
 * 主要功能包含:
 * 1. 根據使用者權限取得對應的選單項目,並支援快取以提升效能。
 * 2. 支援從外部檔案(menuItemsData.json)或專案資源載入選單結構,方便選單維護與部署。
 * 3. 提供選單資料與資料庫(SysMenuDataEntity)之間的轉換功能,支援選單資料的持久化與還原。
 * 4. 根據使用者名稱與權限,動態產生符合權限的選單結構。
 * 5. 支援特殊權限角色(如 IT_ADMIN、SYS_ADMIN 等)的判斷與加入。
 *
 * 典型使用情境:
 * - 使用者登入後,根據其角色權限取得對應的選單項目。
 * - 系統管理員可透過外部檔案或資料庫調整選單結構,無須重新部署應用程式。
 *
 * 注意事項:
 * - 本服務使用 Spring Cache 機制,請確保快取設定正確。
 * - 選單資料結構為遞迴巢狀,請注意資料格式正確性。
 * </pre>
 *
 * @author Lewis
 * @version 1.0
 * @since 2024-06
 */
@Slf4j
@Service
public class PageMenuItemService {
    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * 
     * Constructs a new SystemMenuItemService instance.
     * This is the default constructor, implicitly provided by the compiler
     */
    public PageMenuItemService() {
        // Constructor body (can be empty)
    }


    @Autowired
    private SysMenuDataRepository sysMenuDataRepository;

    @Autowired
    GetSysParmValueService getSysParmValueService;

    /**
     *
     * <pre>
     * 取得 menuItemsData.json的資料後
     * 設定給 MenuItem Model (也可以考慮設計為讀取 DB資料)
     *
     * 使用了Catch功能,所以每個使用者依權限,只會準備一次Menu 。
     * </pre>
     *
     * @param username user name
     * @return Base MenuItems Model
     */
    // @Cacheable(cacheNames = "catchUserMenuItems", key = "#username", cacheManager = "caffeineCacheManager")
     @Caching(cacheable = {
        @Cacheable(cacheNames = "catchUserMenuItems", key = "#username", cacheManager = "caffeineCacheManager"),
        @Cacheable(cacheNames = "catchUserMenuItems", key = "#username", cacheManager = "redisCacheManager")
    })
    public PageMenuItemModel getUserMenuItems(String username) {
        PageMenuItemModel pageMenuItems = PageConstants.pageMenuItems;
        PageMenuItemModel userMenuItems = new PageMenuItemModel();
        if (StringUtils.isBlank(username) || "anonymousUser".equalsIgnoreCase(username)) {
            return userMenuItems;
        }

        // 取得 User Roles (包含特殊權限)
        List<String> userRoles = getUserRoles(username);

        // 設定 UserMenu的 BarText
        userMenuItems.setMenuBarText(pageMenuItems.getMenuBarText());

        // 第一層 NaviMenuItems Loop
        List<PageMenuItemModel.MenuItems> listUserNaviItems = new ArrayList<>();
        pageMenuItems.getNaviItems().forEach(x -> {
            PageMenuItemModel.MenuItems oneNaviItems = x; // 處理第一層Menu
            List<String> listNaviMenuRoles = CommUtils.splitDelimiter(oneNaviItems.getMenuRoles(), ";");
            Boolean hasNaviMenuRole = userHasMenuRoles(userRoles, listNaviMenuRoles);
            if (hasNaviMenuRole || "ALL".equalsIgnoreCase(oneNaviItems.getMenuRoles())) {
                if (oneNaviItems.getIsMenu()) {
                    PageMenuItemModel.MenuItems oneUserNaviItem = getUserOneCollapseItems(userRoles, oneNaviItems);
                    listUserNaviItems.add(oneUserNaviItem);
                } else {
                    listUserNaviItems.add(oneNaviItems);
                }
            }
        });
        userMenuItems.setNaviItems(listUserNaviItems);

        return userMenuItems;
    }

    /**
     * 根據使用者角色過濾並取得可用的第一層子選單項目。
     *
     * 此方法會遞迴處理傳入的選單項目,僅保留使用者有權限的子選單。 若子選單標記為 "ALL",則所有角色皆可存取。
     *
     *
     * @param userRoles    使用者所擁有的角色清單
     * @param oneNaviItems 第一層選單項目
     * @return 過濾後僅包含使用者可存取子選單的 MenuItems 物件
     */
    private PageMenuItemModel.MenuItems getUserOneCollapseItems(List<String> userRoles,
            PageMenuItemModel.MenuItems oneNaviItems) {
        PageMenuItemModel.MenuItems userCollapseItems = new PageMenuItemModel.MenuItems();

        List<PageMenuItemModel.MenuItems> userListCollapseItems = new ArrayList<>();
        oneNaviItems.getCollapseItems().forEach(x -> {
            PageMenuItemModel.MenuItems oneCollapseItems = x; // 處理第一層Menu
            List<String> listCollapseMenuRoles = CommUtils.splitDelimiter(oneCollapseItems.getMenuRoles(), ";");
            Boolean hasCollapseMenuRole = userHasMenuRoles(userRoles, listCollapseMenuRoles);
            if (hasCollapseMenuRole || "ALL".equalsIgnoreCase(oneCollapseItems.getMenuRoles())) {
                if (oneCollapseItems.getIsMenu()) {
                    // recursive 遞迴
                    PageMenuItemModel.MenuItems oneUserCollapseItemsItem = getUserOneCollapseItems(userRoles,
                            oneCollapseItems);
                    userListCollapseItems.add(oneUserCollapseItemsItem);
                } else {
                    userListCollapseItems.add(oneCollapseItems);
                }
            }
        });
        userCollapseItems.setMenuDesc(oneNaviItems.getMenuDesc());
        userCollapseItems.setCollapseItems(userListCollapseItems);
        return userCollapseItems;
    }

    // 確認 Menu Roles 有包含 User Roles 之一
    private Boolean userHasMenuRoles(List<String> userRoles, List<String> listMenuRoles) {
        listMenuRoles.replaceAll(String::toUpperCase);
        userRoles.replaceAll(String::toUpperCase);
        Boolean hasMenuRoles = false;
        for (int i = 0; i < userRoles.size(); i++) {
            String oneUserRole = userRoles.get(i);
            if (listMenuRoles.contains(oneUserRole)) {
                hasMenuRoles = true;
            }
        }
        return hasMenuRoles;
    }

    /**
     * 取得系統的基礎選單項目,並依照選單順序進行多層排序。
     *
     * 此方法會先嘗試從外部檔案取得選單資料,若失敗則從專案資源載入。取得資料後, 會對主選單(第一層)、子選單(第二層)及次子選單(第三層)依據
     * {@code menuSeq} 欄位進行排序。
     *
     *
     * 此方法結果會快取於 {@code catchUserMenuItems},快取鍵為方法名稱。
     *
     *
     * @return 已排序的 PageMenuItemModel 物件,包含所有層級的選單項目。
     * https://www.1ju.org/article/spring-two-level-cache
     */
    @Caching(cacheable = {
        @Cacheable(cacheNames = "catchUserMenuItems", key = "#root.methodName", cacheManager = "caffeineCacheManager"),
        @Cacheable(cacheNames = "catchUserMenuItems", key = "#root.methodName", cacheManager = "redisCacheManager")
    })
    public PageMenuItemModel getPageMenuItems() {
        // 從外部檔案取得系統 PageMenu (這樣改Menu 不需要再打包以及Deploy war)
        PageMenuItemModel pageMenuItems = getPageMenuItemsExternalData();
        if (pageMenuItems == null) {
            // 從外部取得錯誤,再從專案Resource取得 PageMenuItem
            pageMenuItems = getPageMenuItemsFromResource();
        }

        // Menu Item 排序 // https://www.ewdna.com/2008/10/list.html
        // for 主選單 NavItems (第一層)
        Collections.sort(pageMenuItems.getNaviItems(), // sort MenuItem (第一層 Menu)
                new Comparator<PageMenuItemModel.MenuItems>() {
                    public int compare(PageMenuItemModel.MenuItems menuItem1, PageMenuItemModel.MenuItems menuItem2) {
                        return menuItem1.getMenuSeq() - menuItem2.getMenuSeq();
                    }
                }); // end Collections.sort

        // fro 子選單內 collapseItems (第二層)
        pageMenuItems.getNaviItems().forEach(x -> {
            PageMenuItemModel.MenuItems collapseItems = x;
            if (collapseItems.getIsMenu()) {
                Collections.sort(collapseItems.getCollapseItems(), // Sorting collapseItems (第二層 Menu)
                        new Comparator<PageMenuItemModel.MenuItems>() {
                            public int compare(PageMenuItemModel.MenuItems menuItem1,
                                    PageMenuItemModel.MenuItems menuItem2) {
                                return menuItem1.getMenuSeq() - menuItem2.getMenuSeq();
                            }
                        });// end Collections.sort
                // for 子選單內選單 subCollapseItems (第三層)
                collapseItems.getCollapseItems().forEach(y -> {
                    PageMenuItemModel.MenuItems oneSubCollapseItems = y; // Sorting subCollapseItems (第三層 Menu)
                    if (oneSubCollapseItems.getIsMenu()) {
                        Collections.sort(oneSubCollapseItems.getCollapseItems(),
                                new Comparator<PageMenuItemModel.MenuItems>() {
                                    public int compare(PageMenuItemModel.MenuItems menuItem1,
                                            PageMenuItemModel.MenuItems menuItem2) {
                                        return menuItem1.getMenuSeq() - menuItem2.getMenuSeq();
                                    }
                                });// end Collections.sort
                    }

                });
            }
        });
        return pageMenuItems;
    }

    /**
     * 從外部 JSON 檔案讀取系統選單資料,並轉換為 {@link PageMenuItemModel} 物件。
     *
     * 此方法會嘗試從指定的外部資料夾({@link GlobalConstants#EXTERNAL_FOLDER})下的
     * <code>menuItemsData.json</code> 檔案載入選單資料。若檔案不存在或解析失敗,將回傳 <code>null</code>,
     * 並於主控台輸出錯誤訊息。
     *
     *
     * @return 讀取到的 {@link PageMenuItemModel} 物件,若失敗則回傳 <code>null</code>
     */
    private PageMenuItemModel getPageMenuItemsExternalData() {
        PageMenuItemModel rtnPageMenuItemModel = new PageMenuItemModel();
        ObjectMapper objectMapper = new ObjectMapper();
        String externalMenuDataFile = GlobalConstants.EXTERNAL_FOLDER + "/menuItemsData.json";
        try {
            // 從外部檔案取得系統 PageMenu menuItemsData (這樣改Menu 不需要在打包以及Deploy war)
            rtnPageMenuItemModel = objectMapper.readValue(new File(externalMenuDataFile), PageMenuItemModel.class);
        } catch (Exception ex) { // new File => FileNotFoundException or objectMapper exception
            log.info("external menuItemsData {} not found"   , externalMenuDataFile );
            return null;
        }
        return rtnPageMenuItemModel;
    }

    /**
     * 從專案的資源檔案(menuItemsData.json)讀取並解析為 PageMenuItemModel 物件。
     *
     * 此方法會嘗試從 classpath 下的 menuItemsData.json 檔案載入資料,並使用 Jackson 的 ObjectMapper 進行
     * JSON 反序列化。如果讀取或解析過程中發生例外,將回傳 null。
     *
     *
     * @return 解析後的 PageMenuItemModel 物件,若發生錯誤則回傳 null。
     */
    private PageMenuItemModel getPageMenuItemsFromResource() {

        PageMenuItemModel rtnPageMenuItemModel = new PageMenuItemModel();
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            // 從專案 Resource 檔案取得系統 PageMenu menuItemsData
            rtnPageMenuItemModel = objectMapper.readValue(
                    getClass().getClassLoader().getResourceAsStream("menuItemsData.json"),
                    new TypeReference<PageMenuItemModel>() {
                    });
        } catch (Exception ex) { // new File => FileNotFoundException or objectMapper exception
            log.info("menuItemsData.json in project resource");
            return null;
        }
        return rtnPageMenuItemModel;
    }

    /**
     * 根據傳入的 PageMenuItemModel
     * 物件,產生對應的系統選單資料並儲存至資料庫。
     *
     * 此方法會將傳入的根選單資料轉換為
     * {@link tw.lewishome.webapp.database.primary.entity.SysMenuDataEntity},並設定相關屬性。
     * 若根選單包含子選單項目,則會先清空所有現有選單資料,接著儲存根選單資料, 並遞迴將所有子選單項目加入資料庫。
     *
     *
     * @param pageMenuItemModel 傳入的基礎選單項目模型,若為 {@code null} 則直接回傳 {@code null}
     * @return 原始的 PageMenuItemModel 物件
     */
    public PageMenuItemModel genSysMenuDataFromPageMenuItems(PageMenuItemModel pageMenuItemModel) {
        if (pageMenuItemModel == null) {
            return null;
        }
        // 轉換至 SysMenuDataEntity
        SysMenuDataEntity sysMenuDataEntity = new SysMenuDataEntity();
        SysMenuDataEntity.DataKey dataKey = new SysMenuDataEntity.DataKey();

        sysMenuDataEntity.setDataKey(dataKey); // get root uuid
        sysMenuDataEntity.setMenuSeq(0); // Set Root Menu Seq
        sysMenuDataEntity.setIsMenu(true); // Set Root is Menu // not used
        sysMenuDataEntity.setMenuDesc(pageMenuItemModel.getMenuBarText()); // Set Root Menu Desc as menuBarText
        sysMenuDataEntity.setMenuUrl(""); // Set Root Menu Path
        sysMenuDataEntity.setMenuFunc(""); // Set Root Menu Func
        sysMenuDataEntity.setMenuParentUuid("-1"); // Set Root Menu Parent UUID
        sysMenuDataEntity.setMenuRoles(""); // Set Root Menu Roles

        // 處理 Child Menu Items
        if (pageMenuItemModel.getNaviItems() != null && pageMenuItemModel.getNaviItems().size() > 0) {
            sysMenuDataRepository.deleteAll();// 先刪除全部資料
            sysMenuDataRepository.save(sysMenuDataEntity); // 先存Root Menu
            List<PageMenuItemModel.MenuItems> listChildMenuItems = pageMenuItemModel.getNaviItems();
            String oneMenuParentUuid = sysMenuDataEntity.getDataKey().getUuid(); // Child Menu Items的 Parent Uuid
            for (int i = 0; i < listChildMenuItems.size(); i++) {
                PageMenuItemModel.MenuItems oneChildMenuItems = listChildMenuItems.get(i);
                addChildMenuItemToDB(oneChildMenuItems, oneMenuParentUuid);
            }
        }
        return pageMenuItemModel;
    }

    private void addChildMenuItemToDB(PageMenuItemModel.MenuItems oneChildMenuItems, String oneMenuParentUuid) {
        SysMenuDataEntity oneSysMenuDataEntity = new SysMenuDataEntity();
        SysMenuDataEntity.DataKey oneDataKey = new SysMenuDataEntity.DataKey();
        oneSysMenuDataEntity.setDataKey(oneDataKey); // get root uuid
        oneSysMenuDataEntity.setMenuSeq(oneChildMenuItems.getMenuSeq()); // Set Root Menu Seq
        oneSysMenuDataEntity.setIsMenu(oneChildMenuItems.getIsMenu()); // Set Root is Menu // not used
        oneSysMenuDataEntity.setMenuDesc(oneChildMenuItems.getMenuDesc()); // Set Root Menu Desc as menuBarText
        oneSysMenuDataEntity.setMenuUrl(oneChildMenuItems.getMenuUrl()); // Set Root Menu Path
        oneSysMenuDataEntity.setMenuFunc(oneChildMenuItems.getMenuFunc()); // Set Root Menu Func
        oneSysMenuDataEntity.setMenuParentUuid(oneMenuParentUuid); // Set Root Menu Parent UUID
        oneSysMenuDataEntity.setMenuRoles(oneChildMenuItems.getMenuRoles()); // Set Root Menu Roles
        sysMenuDataRepository.save(oneSysMenuDataEntity); // 存入 SysMenuDataEntity
        List<PageMenuItemModel.MenuItems> listChildMenuItems = oneChildMenuItems.getCollapseItems();

        if (oneChildMenuItems.getCollapseItems() != null && oneChildMenuItems.getCollapseItems().size() > 0) {
            String childMenuParentUuid = oneSysMenuDataEntity.getDataKey().getUuid(); // Child Menu Items的 Parent Uuid
            for (int i = 0; i < listChildMenuItems.size(); i++) {
                PageMenuItemModel.MenuItems newChildMenuItems = listChildMenuItems.get(i);
                addChildMenuItemToDB(newChildMenuItems, childMenuParentUuid);
            }
        }
    }

    /**
     * 根據資料庫中的系統選單資料產生 PageMenuItemModel。
     *
     * 此方法會從資料庫取得 Root Menu(Parent Uuid = -1), 若找不到或有多個 Root Menu,則回傳 null 並顯示警告訊息。
     * 若成功取得 Root Menu,則設定 menuBarText 及其子選單項目。
     *
     *
     * @return 產生的 PageMenuItemModel,若無法正確取得 Root Menu 則回傳 null。
     */
    public PageMenuItemModel genPageMenuItemsFromSysMenuData() {
        PageMenuItemModel pageMenuItemModel = new PageMenuItemModel();
        // 取得 Root Menu (Parent Uuid = -1)
        List<SysMenuDataEntity> listRootMenu = sysMenuDataRepository.findByMenuParentUuid("-1");
        if (listRootMenu == null || listRootMenu.size() == 0) {
            log.info("Error: SysMenuDataEntity Root Menu not found");
            return null;
        } else {
            if (listRootMenu.size() > 1) {
                log.info("Error: Duplicate Root Menu SysMenuDataEntity > 1");
                return null;
            }
        }
        // Root Menu (Menu BarText)
        SysMenuDataEntity rootMenu = listRootMenu.get(0);
        pageMenuItemModel.setMenuBarText(rootMenu.getMenuDesc());
        String rootMenuUuid = rootMenu.getDataKey().getUuid();
        List<PageMenuItemModel.MenuItems> rootMenuItems = addChildMenuItemFromDB(listRootMenu.get(0), rootMenuUuid);
        pageMenuItemModel.setNaviItems(rootMenuItems);
        return pageMenuItemModel;

    }

    private List<PageMenuItemModel.MenuItems> addChildMenuItemFromDB(SysMenuDataEntity oneCSysMenuDataEntity,
            String rootMenuUuid) {
        List<PageMenuItemModel.MenuItems> rtnMenuItems = new ArrayList<>();
        // 取得 parentMenuUuid SysMenuData
        List<SysMenuDataEntity> listRootMenu = sysMenuDataRepository.findByMenuParentUuid(rootMenuUuid);

        if (listRootMenu == null || listRootMenu.size() == 0) {
            return null;
        }

        for (int i = 0; i < listRootMenu.size(); i++) {
            SysMenuDataEntity oneChildSysMenuDataEntity = listRootMenu.get(i);
            PageMenuItemModel.MenuItems oneChildMenuItems = new PageMenuItemModel.MenuItems();
            oneChildMenuItems.setMenuSeq(oneChildSysMenuDataEntity.getMenuSeq());
            oneChildMenuItems.setIsMenu(oneChildSysMenuDataEntity.getIsMenu());
            oneChildMenuItems.setMenuDesc(oneChildSysMenuDataEntity.getMenuDesc());
            oneChildMenuItems.setMenuUrl(oneChildSysMenuDataEntity.getMenuUrl());
            oneChildMenuItems.setMenuFunc(oneChildSysMenuDataEntity.getMenuFunc());
            oneChildMenuItems.setMenuRoles(oneChildSysMenuDataEntity.getMenuRoles());
            String parentMenuUuid = oneChildSysMenuDataEntity.getDataKey().getUuid(); // Child Menu Items的 Parent Uuid
            List<PageMenuItemModel.MenuItems> rootMenuItems = addChildMenuItemFromDB(oneChildSysMenuDataEntity,
                    parentMenuUuid);
            if (rootMenuItems != null && rootMenuItems.size() > 0) {
                oneChildMenuItems.setCollapseItems(rootMenuItems);
            } else {
                oneChildMenuItems.setCollapseItems(null);
            }
            rtnMenuItems.add(oneChildMenuItems);
        }
        return rtnMenuItems;
    }

    /**
     * 將以分號(;)分隔的角色字串分割成清單,並將所有角色名稱轉換為大寫。
     *
     * @param pageMenuRoles 以分號分隔的角色名稱字串
     * @return 轉換為大寫後的角色名稱清單
     */
    public List<String> getListMenuRoles(String pageMenuRoles) {
        List<String> rtnMenuRoles = new ArrayList<>();
        String[] splitMenuRoles = pageMenuRoles.split(";");
        for (int i = 0; i < splitMenuRoles.length; i++) {
            rtnMenuRoles.add(splitMenuRoles[i]);
        }
        // 全部需要轉換大寫
        rtnMenuRoles.replaceAll(String::toUpperCase);

        return rtnMenuRoles;
    }

    /**
     * 根據使用者名稱取得該使用者的所有角色清單。
     *
     * 此方法會依序執行下列步驟:
     *  <ul>
     * <li>從 Spring Security 的 Authentication 物件取得使用者的權限角色。</li>
     * <li>根據系統參數資料(如
     * IT_ADMIN、SYS_ADMIN、USER_ADMIN、OP_ADMIN)判斷使用者是否擁有特殊角色,若有則加入角色清單。</li>
     * <li>將使用者名稱(轉為大寫)作為一個角色加入清單。</li>
     * <li>最後將所有角色名稱轉為大寫後回傳。</li>
     *  </ul>
     *
     * @param username 使用者名稱
     * @return 包含所有角色名稱(皆為大寫)的清單
     */
    private List<String> getUserRoles(String username) {
        List<String> rtnUserRoles = new ArrayList<>();

        // Add user roles from Authentication
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null) {
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            if (authorities.size() > 0) {
                rtnUserRoles = getAuthoritiesRoles(authorities);
            }
        }

        // Add special User Roles from SysUserProfileEntity

        List<String> specialRoles = new ArrayList<>(Arrays.asList("IT_ADMIN", "SYS_ADMIN", "USER_ADMIN", "OP_ADMIN"));
        for (int i = 0; i < specialRoles.size(); i++) {
            String oneSpecialRole = specialRoles.get(i).trim();
            if (hasSpecialRoles(username, oneSpecialRole)) {
                rtnUserRoles.add(oneSpecialRole);
            }
        }

        // Add User Name for one of Roles
        rtnUserRoles.add(username.trim().toUpperCase());

        // 全部需要轉換大寫
        rtnUserRoles.replaceAll(String::toUpperCase);
        return rtnUserRoles;
    }

    /**
     * 取得使用者權限角色清單。
     *
     * 此方法會將傳入的 {@link GrantedAuthority} 集合中的每個權限字串進行處理, 取得去除 "ROLE_"
     * 前綴後的角色名稱,若角色名稱包含 "Dept_",則會再進一步去除 "Dept_" 前綴。 最終回傳處理後的角色名稱字串清單。
     *
     *
     * @param authorities 權限集合,來源為 Spring Security 的 {@link GrantedAuthority}
     * @return 處理後的角色名稱字串清單
     */
    private List<String> getAuthoritiesRoles(Collection<? extends GrantedAuthority> authorities) {
        List<String> rtnUserRoles = new ArrayList<>();
        authorities.forEach(x -> {
            String authority = x.getAuthority();
            String oneRole;
            
            // 檢查是否以 "ROLE_" 開頭,如果是則移除前綴
            if (authority.startsWith("ROLE_")) {
                oneRole = authority.substring(5); // "ROLE_" 的長度是 5
            } else {
                oneRole = authority;
            }
            // 檢查是否以 "OIDC_" 開頭,如果是則移除前綴
            if (authority.startsWith("OIDC_")) {
                oneRole = authority; // OIDC_ 不移除
            } else {
                oneRole = authority;
            }

            // 如果角色名稱包含 "Dept_",則去除 "Dept_" 前綴
            if (oneRole.contains("Dept_")) {
                String[] parts = oneRole.split("_", 2); // 限制分割為最多 2 個部分
                if (parts.length > 1) {
                    oneRole = parts[1];
                }
            }
            rtnUserRoles.add(oneRole);
        });
        return rtnUserRoles;
    }

    /**
     * 檢查指定使用者是否擁有特定的特殊角色。
     *
     * @param username       使用者名稱。
     * @param oneSpecialRole 欲檢查的特殊角色參數名稱。
     * @return 若使用者擁有該特殊角色則回傳 true,否則回傳 false。
     */
    private Boolean hasSpecialRoles(String username, String oneSpecialRole) {
        Boolean hasRoles = false;
        List<String> listSysParmValue = getSysParmValueService.getSysParmDataEntityValue(oneSpecialRole);
        for (int j = 0; j < listSysParmValue.size(); j++) {
            String oneSysParmValue = listSysParmValue.get(j);
            List<String> splitUsers = CommUtils.splitDelimiter(oneSysParmValue, ";");
            if (splitUsers.contains(username)) {
                hasRoles = true;
                break;
            }
        }
        return hasRoles;
    }

}

  1. PageControllerBase.java (new 前端服務處理Controller基本程式)
  • 提供共用的PageControllerBase 給所有Controller繼承
  • 基本包含 SessionAttributes ,以及設定Session 變數內容
package tw.lewishome.webapp.page.base.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
// import org.springframework.web.util.WebUtils;

import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import tw.lewishome.webapp.page.base.model.PageMenuItemModel;

/**
 * <pre>
 * 提供共用的PageControllerBase 給所有Controller繼承。
 * 基本包含 SessionAttributes ,以及設定Session 變數內容
 * </pre>
 *
 * @author lewis
 * @version $Id: $Id
 */

@Slf4j
@SessionAttributes({ "isLayoutTop", "menuDesc", "menuFunc", "pageButtonId",
        "showAddBtn", "ajaxButtonString", "menuUrl", "headerDataString" })
public abstract class PageControllerBase {
    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * Constructs a new BasePageController instance.
     * This is the default constructor, implicitly provided by the compiler
     * if no other constructors are defined.
     * 
     */

    @Autowired
    protected PageMenuItemService pageMenuItemService;

    public PageControllerBase() {
        // Constructor body (can be empty)
    }

    /**
     * 設定Session 變數內容
     *
     * @param model   Session Model
     * @param request 前端給的 Request
     */
    @ModelAttribute
    public void addAttributes(Model model, HttpServletRequest request) {
        log.info("PageControllerBase - addAttributes called - getRequestURI: ", request.getRequestURI());
        // 增加動態 nonce變數內容到 Session變數
        model.addAttribute("nonce", request.getAttribute("cspNonce"));
        // WebUtils.getSessionAttribute(request, "currentUser");
        // 取得 Login User
        Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        String userId = null;

        if (principal instanceof OAuth2User oauth2User) {
            // OAuth2 授權使用 oauth2User 取得 userId (Email)
            String email = (String) oauth2User.getAttribute("email");
            userId = email != null ? email : oauth2User.getName();

        } else if (principal instanceof UserDetails userDetails) {
            // CustomAuthenticationProvider 授權使用 UserDetails 取得 userId
            userId = userDetails.getUsername();
        } else {
            // 其他情況直接使用 principal 的 toString() 方法
            userId = principal.toString();
        }
        model.addAttribute("userId", userId);

        // 因為getMenuItems 有 Catch 所以 pageMenuItemModel 不用使用Session 變數
        // anonymousUser (Security 白名單授權),不需要載入系統選單Menu
        if (!userId.equalsIgnoreCase("anonymousUser")) {
            // 依 Login User準備 Menu Items
            PageMenuItemModel pageMenuItemModel = pageMenuItemService.getUserMenuItems(userId);
            model.addAttribute("MenuItemModelVar", pageMenuItemModel); // 設定 Session 的 MenuItemModel
        }

        // 從 Session 取得 isLayoutTop
        Boolean layoutTopVal = (Boolean) model.getAttribute("isLayoutTop");
        if (layoutTopVal == null) { // Session 沒有 layoutTopVal 設為 false(SideAMenu)
            model.addAttribute("isLayoutTop", true); // 設定 Session 的 layoutTopVal

        }

        // 從 Session 取得 menuFunc (是從Menu Item 點選的請求)
        String menuFunc = (String) model.getAttribute("menuFunc");
        model.addAttribute("menuFunc", menuFunc);

        String menuUrl = (String) model.getAttribute("menuUrl");
        model.addAttribute("menuUrl", menuUrl);

    }
}


6 Spring Boot 應用系統跟目錄(HomePage)以及 Page 功能常變數。

位置 package tw.lewishome.webapp.page;

  1. PageConstants.java (new 前端服務處理的使用常變數)
package tw.lewishome.webapp.page;

import tw.lewishome.webapp.page.base.model.PageMenuItemModel;

public class PageConstants {

        /** Private constructor to prevent instantiation */
        PageConstants() {
                throw new UnsupportedOperationException("This is a utility class and cannot be instantiated.");
        }

        /** 系統選單Menu 於由SystemApplicationListener 設定 */
        public static PageMenuItemModel pageMenuItems = new PageMenuItemModel();

                /** Spring Aspect PointCut Package 設定 */
        public static final String ASPECT_PACKAGE = "execution(public * tw.lewishome.webapp.**page.controller.*.*(..))";

}

  1. HomePageController.java (revise 增加URL處理程序)
package tw.lewishome.webapp.page;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import jakarta.servlet.http.HttpServletRequest;
import tw.lewishome.webapp.GlobalConstants;
import tw.lewishome.webapp.base.utility.common.NetUtils;
import tw.lewishome.webapp.database.primary.entity.SysAccessLogEntity;
import tw.lewishome.webapp.database.primary.repository.SysAccessLogRepository;
import tw.lewishome.webapp.database.primary.service.SysUserProfileService;
import tw.lewishome.webapp.page.base.service.PageControllerBase;
import org.springframework.core.ResolvableType;









/**
 * <pre>
 * 系統跟目錄(Home)的控制服務程式
 *
 * 系統跟目錄(Home)的控制服務程式,包含以下主要功能 <br/>
 * </pre>
 *
 * @author Lewis
 * @version $Id: $Id
 */
@Controller // 宣告是 Controller 類別
public class HomePageController extends PageControllerBase {
    /**
     * Fix for javadoc warning :
     * use of default constructor, which does not provide a comment
     * Constructs a new HomePageController instance.
     * This is the default constructor, implicitly provided by the compiler
     * if no other constructors are defined.
     */
    public HomePageController() {
        // Constructor body (can be empty)
    }

    @Autowired
    private SysAccessLogRepository sysAccessLogRepository;

    @Autowired
    SysUserProfileService sysUserProfileService;

    private static String authorizationRequestBaseUri = "oauth2/authorization";
    Map<String, String> oauth2AuthenticationUrls = new HashMap<>();
    @Autowired
    private ClientRegistrationRepository clientRegistrationRepository;

    /**
     * <pre>
     * 接受前端 URL ==> "/home" or "/index" or "/的 Get request
     * 顯示前端 /home/homePage.html
     * </pre>
     *
     * @param model Session model
     * @return String 前端Html
     */
    @SuppressWarnings({ "unchecked", "null" })
    @GetMapping({ "/home", "/index", "/" })
    public String getHomeForm(Model model) {

         // 回覆前端的 html ()
        Iterable<ClientRegistration> clientRegistrations = null;
        ResolvableType type = ResolvableType.forInstance(clientRegistrationRepository)
                .as(Iterable.class);
        if (type != ResolvableType.NONE &&
                ClientRegistration.class.isAssignableFrom(type.resolveGenerics()[0])) {
            clientRegistrations = (Iterable<ClientRegistration>) clientRegistrationRepository;
        }
        clientRegistrations.forEach(registration -> oauth2AuthenticationUrls.put(registration.getClientName(),
                authorizationRequestBaseUri + "/" + registration.getRegistrationId()));
        model.addAttribute("urls", oauth2AuthenticationUrls);
        // 回覆前端的 html ()
        return "/home/homePage.html";
    }

    /**
     * 接受Login認證後 導向 "/landing" 的 getRequest
     *
     * @param model page Model
     * @return String /home/landingPage.html
     */
    @GetMapping({ "/landing" })
    public String doLandingForm(Model model) {
        model.addAttribute("menuFunc", "landing");
        model.addAttribute("menuUrl", "landing");

        return "/home/landingPage.html";
    }

    /**
     * 處理 Menu點選的 URL (統一Menu入口,先處理Menu共同需要的程序)
     *
     * @param model    Page Model
     * @param request  HttpServletRequest
     * @param menuUrl  轉向 Menu 指定 URL
     * @param menuDesc Menu說明 (功能 Title)
     * @param menuFunc Menu功能 (查詢/維護)
     * @return String menuUrl
     */
    @GetMapping("doMenu")
    public String doRedirectMenuUrl(Model model, HttpServletRequest request,
            @RequestParam("Url") String menuUrl,
            @RequestParam("Desc") String menuDesc,
            @RequestParam("Func") String menuFunc) {

        // html 會使用
        model.addAttribute("menuDesc", menuDesc);
        // html 會使用 在Post Endpoint時傳給後端使用
        // model.addAttribute("isMenuUrl", true);

        model.addAttribute("menuFunc", menuFunc);

        model.addAttribute("menuUrl", menuUrl);

        // headerDataString
        model.addAttribute("headerDataString", "");

        // SysAccessLogS
        SysAccessLogEntity oneSAccessLogEntity = new SysAccessLogEntity();
        SysAccessLogEntity.DataKey dataKey = new SysAccessLogEntity.DataKey();
        oneSAccessLogEntity.setDataKey(dataKey);
        oneSAccessLogEntity.setSessionId(request.getSession().getId());
        oneSAccessLogEntity.setRemoteIp(NetUtils.getClientIpAddress(request));
        oneSAccessLogEntity.setServerName(GlobalConstants.HOST_SERVER_NAME);
        oneSAccessLogEntity.setAccessUrl(menuUrl + ":" + menuFunc);
        String userId = (String) model.getAttribute("userId");
        oneSAccessLogEntity.setAccessUser(userId);
        oneSAccessLogEntity.setAction("Do Menu");
        sysAccessLogRepository.saveAndFlush(oneSAccessLogEntity);

        return "redirect:" + menuUrl;
    }

    /**
     * 處理 "/chgMenuLayout" 的 Get request
     *
     * @param model Page Model
     * @return String redirect 網址或前端Html
     */
    @GetMapping({ "/chgMenuLayout" })
    public String chgMenuLayout(Model model) {
        // 從 Session 取得 isLayoutTop
        Boolean layoutTopVal = (Boolean) model.getAttribute("isLayoutTop");
        if (layoutTopVal == null) { // Session 沒有 layoutTopVal 設為 true(topMenu)
            layoutTopVal = true;
        }
        layoutTopVal = !layoutTopVal; // layoutTopVal 做 not 運算切換 layoutTopVal
        model.addAttribute("isLayoutTop", layoutTopVal); // 設定 Session 的 layoutTopVal
        // 切換 Layout後,從 Session 取得當前 URL (menuUrl)
        String menuUrl = (String) model.getAttribute("menuUrl");
        if (StringUtils.isBlank(menuUrl)) { // 從 Session 沒有當前 URL (menuUrl)
            menuUrl = "/"; // 設定當前URL (menuUrl) 為跟目錄
        }
        model.addAttribute("menuUrl", menuUrl);

        return "redirect:" + menuUrl; // 轉到當前 URL (menuUrl)
    }

}

前端 Thymeleaf layout 公用版面 以及 選單處理 Hhtml

**發文會切斷 ,接續 請參考 URL: (https://ithelp.ithome.com.tw/articles/10398905) **


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言